package com.test.miniotest.minio.utils;

import com.alibaba.fastjson.JSONObject;
import io.minio.*;
import io.minio.errors.*;
import io.minio.http.Method;
import io.minio.messages.Item;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.springframework.web.multipart.MultipartFile;

import javax.servlet.http.HttpServletResponse;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.URLEncoder;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;


@Component
public class MinIOUtil {

    @Autowired
    private MinioClient minioClient;

    //存储分片的存储桶
    //@Value("${minio.bigFileChunkBucketName}")
    //private String chunkBucKet;
    /**
     * 排序
     */
    public final static boolean SORT = true;
    /**
     * 不排序
     */
    public final static boolean NOT_SORT = false;
    /**
     * 默认过期时间(分钟)
     */
    private final static Integer DEFAULT_EXPIRY = 60;


    /**
     * 初始化存储桶
     */
    public void createBucket(String bucketName) throws IOException, InvalidKeyException, InvalidResponseException, InsufficientDataException, NoSuchAlgorithmException, ServerException, InternalException, XmlParserException, ErrorResponseException {
        if (!minioClient.bucketExists(BucketExistsArgs.builder().bucket(bucketName).build())) {
            minioClient.makeBucket(MakeBucketArgs.builder().bucket(bucketName).build());
        }
    }


    /**
     * 验证存储桶是否存在
     */
    public boolean bucketExists(String bucketName) throws IOException, InvalidKeyException, InvalidResponseException, InsufficientDataException, NoSuchAlgorithmException, ServerException, InternalException, XmlParserException, ErrorResponseException {
        return minioClient.bucketExists(BucketExistsArgs.builder().bucket(bucketName).build());
    }


    /**
     * 获取存储桶策略
     *
     * @param bucketName 存储桶名称
     * @return json
     */
    public JSONObject getBucketPolicy(String bucketName)
            throws IOException, InvalidKeyException, InvalidResponseException, BucketPolicyTooLargeException, NoSuchAlgorithmException, ServerException, InternalException, XmlParserException, InsufficientDataException, ErrorResponseException {
        String bucketPolicy = minioClient.getBucketPolicy(GetBucketPolicyArgs.builder()
                .bucket(bucketName)
                .build());
        return JSONObject.parseObject(bucketPolicy);
    }


    /**
     * 上传文件,成功返回服务器存储的文件完整路径
     *
     * @param bucketName    存储桶
     * @param filePath      文件存放路径（必须以/开始和以/结尾），不包含文件名。例如：/file/pdf/2022/2/23/
     * @param multipartFile
     * @return 文件存储完整地址 如：/file/pdf/2022/2/23/1645404169.pdf
     */
    public String upload(String bucketName, String filePath, MultipartFile multipartFile)
            throws IOException, InvalidKeyException, InvalidResponseException, InsufficientDataException, NoSuchAlgorithmException, ServerException, InternalException, XmlParserException, ErrorResponseException {
        if (StringUtils.isBlank(bucketName)) {
            throw new NullPointerException("bucketName是空的!");
        }
        if (StringUtils.isBlank(filePath)) {
            throw new NullPointerException("filePath是空的!");
        }
        // 文件名
        String originalFilename = multipartFile.getOriginalFilename();
        // 新的文件名 = 存储桶名称_时间戳.后缀名
        String objectName = bucketName + "_" + System.currentTimeMillis() + originalFilename.substring(originalFilename.lastIndexOf("."));
        ObjectWriteResponse objectWriteResponse = minioClient.putObject(PutObjectArgs.builder()
                .bucket(bucketName)
                .object(filePath + objectName)
                .stream(multipartFile.getInputStream(), multipartFile.getInputStream().available(), -1)
                .contentType("application/octet-stream")
                .build());
        return objectWriteResponse.object();
    }


    /**
     * 通过流上传文件
     *
     * @param bucketName  存储桶
     * @param filePath    文件存放路径（必须以/开始和以/结尾），不包含文件名。例如：/file/pdf/2022/2/23/
     * @param fileName    源文件名（带后缀名）如：abc.docx
     * @param inputStream 文件流
     * @return 文件存储完整地址 如：/file/pdf/2022/2/23/1645404169.pdf
     */
    public String upload(String bucketName, String filePath, String fileName, InputStream inputStream)
            throws IOException, InvalidKeyException, InvalidResponseException, InsufficientDataException, NoSuchAlgorithmException, ServerException, InternalException, XmlParserException, ErrorResponseException {
        // 新的文件名 = 存储桶名称_时间戳.后缀名
        String objectName = bucketName + "_" + System.currentTimeMillis() + fileName.substring(fileName.lastIndexOf("."));
        ObjectWriteResponse objectWriteResponse = minioClient.putObject(PutObjectArgs.builder()
                .bucket(bucketName)
                .object(filePath + objectName)
                .stream(inputStream, inputStream.available(), -1)
                .contentType("application/octet-stream")
                .build());
        return objectWriteResponse.object();
    }


    /**
     * 创建文件夹或目录
     *
     * @param bucketName 存储桶
     * @param objectName 目录路径
     * @return
     */
    public String putDirObject(String bucketName, String objectName)
            throws IOException, InvalidKeyException, InvalidResponseException, InsufficientDataException, NoSuchAlgorithmException, ServerException, InternalException, XmlParserException, ErrorResponseException {
        ObjectWriteResponse objectWriteResponse = minioClient.putObject(PutObjectArgs.builder()
                .bucket(bucketName)
                .object(objectName)
                .stream(new ByteArrayInputStream(new byte[]{}), 0, -1)
                .build());
        return objectWriteResponse.object();
    }


    /**
     * 下载文件
     *
     * @param response
     * @param bucketName 存储桶
     * @param objectName 文件存储路（以/开头，包含文件名），如：/2022/02/01/1645404169.pdf
     * @throws ErrorResponseException
     * @throws InsufficientDataException
     * @throws InternalException
     * @throws InvalidKeyException
     * @throws InvalidResponseException
     * @throws IOException
     * @throws NoSuchAlgorithmException
     * @throws ServerException
     * @throws XmlParserException
     */
    public void download(HttpServletResponse response, String bucketName, String objectName) throws ErrorResponseException, InsufficientDataException, InternalException, InvalidKeyException, InvalidResponseException, IOException, NoSuchAlgorithmException, ServerException, XmlParserException {
        InputStream is = null;
        OutputStream os = null;

        is = minioClient.getObject(GetObjectArgs.builder().bucket(bucketName).object(objectName).build());
        os = response.getOutputStream();
        // 设置响应头
        String encoderName = URLEncoder.encode("测试文件.docx", "UTF-8").replace("+", "%20").replace("%2B", "+");
        response.reset();
        response.setHeader("fileName", encoderName);
        response.setHeader("Content-Disposition", "attachment; filename=\"" + encoderName + "\"");
        //response.addHeader("Content-Length", "" + data.length);
        response.setHeader("Access-Control-Allow-Origin", "" + "*");
        response.setHeader("Access-Control-Allow-Methods", "POST, PUT, GET, OPTIONS, DELETE");
        response.setHeader("Access-Control-Allow-Headers", "Authorization, Content-Type");
        //response.setHeader("Accept-Ranges", "bytes");
        response.setContentType("application/octet-stream" + ";charset=UTF-8");

        byte[] buffer = new byte[1024 * 5];
        int len = 0;
        while ((len = is.read(buffer)) > 0) {
            os.write(buffer, 0, len);
        }
        os.flush();

        is.close();
        os.close();
    }


    /**
     * 下载文件流
     *
     * @param bucketName 存储桶
     * @param objectName 文件存储路径（以/开头，包含文件名），如：/2022/02/01/1645404169.pdf
     * @return 二进制流
     */
    public InputStream downloadStream(String bucketName, String objectName)
            throws IOException, InvalidKeyException, InvalidResponseException, InsufficientDataException, NoSuchAlgorithmException, ServerException, InternalException, XmlParserException, ErrorResponseException {
        InputStream inputStream = minioClient.getObject(GetObjectArgs.builder().bucket(bucketName).object(objectName).build());
        return inputStream;
    }


    /**
     * 拷贝文件
     *
     * @param bucketName    源文件存储桶
     * @param objectName    源文件存储路径（以/开头，包含文件名），如：/2022/02/01/1645404169.pdf
     * @param srcBucketName 目标存储桶
     * @param srcObjectName 目标文件存放路径（必须以/开始和以/结尾），不包含文件名。例如：/file/pdf/2022/2/23/
     * @return 文件存储完整地址 如：/file/pdf/2022/2/23/1645404169.pdf
     */
    public String copyFile(String bucketName, String objectName, String srcBucketName, String srcObjectName)
            throws IOException, InvalidKeyException, InvalidResponseException, InsufficientDataException, NoSuchAlgorithmException, ServerException, InternalException, XmlParserException, ErrorResponseException {
        // 新的文件名 = 存储桶名称_时间戳.后缀名
        String fileName = srcBucketName + "_" + System.currentTimeMillis() + objectName.substring(objectName.lastIndexOf("."));

        ObjectWriteResponse objectWriteResponse = minioClient.copyObject(CopyObjectArgs.builder()
                .source(CopySource.builder().bucket(bucketName).object(objectName).build())
                .bucket(srcBucketName)
                .object(srcObjectName + fileName)
                .build());
        return objectWriteResponse.object();
    }


    /**
     * 删除文件
     *
     * @param bucketName 存储桶
     * @param objectName 文件存储路径（以/开头，包含文件名），如：/2022/02/01/1645404169.pdf
     */
    public void delete(String bucketName, String objectName)
            throws IOException, InvalidKeyException, InvalidResponseException, InsufficientDataException, NoSuchAlgorithmException, ServerException, InternalException, XmlParserException, ErrorResponseException {
        minioClient.removeObject(RemoveObjectArgs.builder().bucket(bucketName).object(objectName).build());
    }


    /**
     * 获取文件外链
     *
     * @param bucketName bucket名称
     * @param objectName 文件名称
     * @param expires    过期时间（秒）有效期最长为 7 天
     * @return url
     */
    public String getPresignedObjectUrl(String bucketName, String objectName, Integer expires)
            throws IOException, InvalidKeyException, InvalidResponseException, InsufficientDataException, ServerException, InternalException, NoSuchAlgorithmException, XmlParserException, ErrorResponseException {
        String presignedObjectUrl = minioClient.getPresignedObjectUrl(GetPresignedObjectUrlArgs.builder().bucket(bucketName).object(objectName).expiry(expires).method(Method.GET).build());
        return presignedObjectUrl;
    }


    // 大文件
    // 大文件
    // 大文件
    // 大文件
    // 大文件
    // 大文件
    // 大文件
    // 大文件
    // 大文件
    // 大文件
    // 大文件

    /**
     * 获取分片名称地址HashMap key=分片序号 value=分片文件地址
     *
     * @param bucketName 存储桶名称
     * @param ObjectMd5  对象Md5
     * @return objectChunkNameMap
     */
    public Map<Integer, String> mapChunkObjectNames(String bucketName, String ObjectMd5) throws Exception {
        //if (null == bucketName) {
        //    bucketName = chunkBucKet;
        //}
        if (null == ObjectMd5) {
            return null;
        }
        List<String> chunkPaths = listObjectNames(bucketName, ObjectMd5);
        if (null == chunkPaths || chunkPaths.size() == 0) {
            return null;
        }
        Map<Integer, String> chunkMap = new HashMap<>(chunkPaths.size());
        for (String chunkName : chunkPaths) {
            Integer partNumber = Integer.parseInt(chunkName.substring(chunkName.indexOf("/") + 1, chunkName.lastIndexOf(".")));
            chunkMap.put(partNumber, chunkName);
        }
        return chunkMap;
    }

    /**
     * 获取分片文件名称列表
     *
     * @param bucketName 存储桶名称
     * @param ObjectMd5  对象Md5
     * @return objectChunkNames
     */
    public List<String> listChunkObjectNames(String bucketName, String ObjectMd5) throws Exception {
        //if (null == bucketName) {
        //    bucketName = chunkBucKet;
        //}
        if (null == ObjectMd5) {
            return null;
        }
        return listObjectNames(bucketName, ObjectMd5, SORT);
    }

    /**
     * 获取对象文件名称列表
     *
     * @param bucketName 存储桶名称
     * @param prefix     对象名称前缀
     * @param sort       是否排序(升序)
     * @return objectNames
     */
    public List<String> listObjectNames(String bucketName, String prefix, Boolean sort) throws Exception {
        ListObjectsArgs listObjectsArgs;
        if (null == prefix) {
            listObjectsArgs = ListObjectsArgs.builder()
                    .bucket(bucketName)
                    .recursive(true)
                    .build();
        } else {
            listObjectsArgs = ListObjectsArgs.builder()
                    .bucket(bucketName)
                    .prefix(prefix)
                    .recursive(true)
                    .build();
        }
        Iterable<Result<Item>> chunks = minioClient.listObjects(listObjectsArgs);
        List<String> chunkPaths = new ArrayList<>();
        for (Result<Item> item : chunks) {
            chunkPaths.add(item.get().objectName());
        }
        if (sort) {
            return chunkPaths.stream().distinct().collect(Collectors.toList());
        }
        return chunkPaths;
    }

    /**
     * 获取对象文件名称列表
     *
     * @param bucketName 存储桶名称
     * @param prefix     对象名称前缀
     * @return objectNames
     */
    public List<String> listObjectNames(String bucketName, String prefix) throws Exception {
        return listObjectNames(bucketName, prefix, NOT_SORT);
    }

    /**
     * 批量创建分片上传外链
     *
     * @param bucketName 存储桶名称
     * @param objectMD5  欲上传分片文件主文件的MD5
     * @param chunkCount 分片数量
     * @return uploadChunkUrls
     */
    public List<String> createUploadChunkUrlList(String bucketName, String objectMD5, Integer chunkCount) throws Exception {
        //if (null == bucketName) {
        //    bucketName = chunkBucKet;
        //}
        if (null == objectMD5) {
            return null;
        }
        objectMD5 += "/";
        if (null == chunkCount || 0 == chunkCount) {
            return null;
        }
        List<String> urlList = new ArrayList<>(chunkCount);
        for (int i = 1; i <= chunkCount; i++) {
            String objectName = objectMD5 + i + ".chunk";
            urlList.add(createUploadUrl(bucketName, objectName, DEFAULT_EXPIRY));
        }
        return urlList;
    }

    /**
     * 创建指定序号的分片文件上传外链
     *
     * @param bucketName 存储桶名称
     * @param objectMD5  欲上传分片文件主文件的MD5
     * @param partNumber 分片序号
     * @return uploadChunkUrl
     */
    public String createUploadChunkUrl(String bucketName, String objectMD5, Integer partNumber) throws Exception {
        //if (null == bucketName) {
        //    bucketName = chunkBucKet;
        //}
        if (null == objectMD5) {
            return null;
        }
        objectMD5 += "/" + partNumber + ".chunk";
        return createUploadUrl(bucketName, objectMD5, DEFAULT_EXPIRY);
    }

    /**
     * 创建上传文件对象的外链
     *
     * @param bucketName 存储桶名称
     * @param objectName 欲上传文件对象的名称
     * @param expiry     过期时间(分钟) 最大为7天 超过7天则默认最大值
     * @return uploadUrl
     */
    public String createUploadUrl(String bucketName, String objectName, Integer expiry) throws Exception {
        expiry = expiryHandle(expiry);
        return minioClient.getPresignedObjectUrl(
                GetPresignedObjectUrlArgs.builder()
                        .method(Method.PUT)
                        .bucket(bucketName)
                        .object(objectName)
                        .expiry(expiry)
                        .build()
        );
    }

    /**
     * 将分钟数转换为秒数
     *
     * @param expiry 过期时间(分钟数)
     * @return expiry
     */
    private int expiryHandle(Integer expiry) {
        expiry = expiry * 60;
        if (expiry > 604800) {
            return 604800;
        }
        return expiry;
    }

    /**
     * 合并分片文件成对象文件
     *
     * @param chunkBucKetName   分片文件所在存储桶名称
     * @param composeBucketName 合并后的对象文件存储的存储桶名称
     * @param chunkNames        分片文件名称集合
     * @param objectName        合并后的对象文件名称
     * @return 文件存储地址
     */
    public String composeObject(String chunkBucKetName, String composeBucketName, List<String> chunkNames, String objectName) throws Exception {
        //if (null == chunkBucKetName) {
        //    chunkBucKetName = chunkBucKet;
        //}
        List<ComposeSource> sourceObjectList = new ArrayList<>(chunkNames.size());
        for (String chunk : chunkNames) {
            sourceObjectList.add(
                    ComposeSource.builder()
                            .bucket(chunkBucKetName)
                            .object(chunk)
                            .build()
            );
        }
        try {

        } catch (Exception e) {
            e.printStackTrace();
        }
        ObjectWriteResponse objectWriteResponse = minioClient.composeObject(
                ComposeObjectArgs.builder()
                        .bucket(composeBucketName)
                        .object(objectName)
                        .sources(sourceObjectList)
                        .build()
        );
        return objectWriteResponse.object();
    }

    /**
     * 合并分片文件成对象文件
     *
     * @param bucketName 存储桶名称
     * @param chunkNames 分片文件名称集合
     * @param objectName 合并后的对象文件名称
     * @return 文件存储地址
     */
    //public String composeObject(String bucketName, List<String> chunkNames, String objectName) throws Exception {
    //    return composeObject(chunkBucKet, bucketName, chunkNames, objectName);
    //}

    /**
     * 获取访问对象的外链地址
     *
     * @param bucketName 存储桶名称
     * @param objectName 对象名称
     * @param expiry     过期时间(分钟) 最大为7天 超过7天则默认最大值
     * @return viewUrl
     */
    public String getObjectUrl(String bucketName, String objectName, Integer expiry) throws Exception {
        expiry = expiryHandle(expiry);
        return minioClient.getPresignedObjectUrl(
                GetPresignedObjectUrlArgs.builder()
                        .method(Method.GET)
                        .bucket(bucketName)
                        .object(objectName)
                        .expiry(expiry)
                        .build()
        );
    }


}
